package com.gitee.huanminabc.utils_tools.redis.aspect.handler;

import com.gitee.huanminabc.utils_tools.redis.aspect.note.RedisLimiter;
import com.gitee.huanminabc.utils_tools.redis.model.bo.RequestInformationBo;
import com.gitee.huanminabc.utils_tools.redis.service.RedisRateLimiterService;
import com.gitee.huanminabc.utils_tools.redis.exception.RateLimiterException;
import com.gitee.huanminabc.utils_tools.web_base.utils.IPUtil;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * redis 限流
 */
@Aspect
@Component
@Slf4j
public class RedisRateLimiterAspect {
    @Autowired
    private RedisRateLimiterService rateLimiter;

    //方法和类上的注解
    @Pointcut("@annotation(redisLimiter)|| @within(redisLimiter)")
    public void pointCut(RedisLimiter redisLimiter) {
    }

    @Around(value = "pointCut(redisLimiter)", argNames = "pjp,redisLimiter")
    public Object around(ProceedingJoinPoint pjp, RedisLimiter redisLimiter) throws Throwable {
        RequestInformationBo requestInformation = new RequestInformationBo();
        buildMethodInfo(requestInformation, pjp);
        buildLimitInfo(requestInformation);
        buildRequestInfo(requestInformation);
        if (rateLimiter.pass(requestInformation)) {
            return pjp.proceed();
        }
        Object[] arguments = requestInformation.getArguments();
        HttpServletRequest httpServletRequest = requestInformation.getHttpServletRequest();
        String requestURI = httpServletRequest.getScheme() + "://" + requestInformation.getIp() + ":" + httpServletRequest.getServerPort() + httpServletRequest.getRequestURI();
        log.info("rateLimiter拦截了一个请求,该请求信息如下: URI: {} 类名:{} , 方法名: {} ,方法参数信息: {} ",
                requestURI, requestInformation.getAClass(), requestInformation.getMethod().getName(), Lists.newArrayList(arguments));
        throw new RateLimiterException(requestInformation.getIp() + " 访问过于频繁,请稍后再试!");
    }

    private void buildLimitInfo(RequestInformationBo requestInformation) throws RateLimiterException {
        Method method = requestInformation.getMethod();
        RedisLimiter limiter;
        if (method.isAnnotationPresent(RedisLimiter.class)) {
            limiter = method.getAnnotation(RedisLimiter.class);
        } else {
            limiter = method.getClass().getAnnotation(RedisLimiter.class);
        }
        if (limiter == null) {
            throw new RateLimiterException("无法在当前方法" + method.getName() + "或者类" + method.getClass().getName() + "上寻找到@Limiter注解");
        }
        requestInformation.setKey(limiter.key());
        requestInformation.setCount(limiter.count());
        requestInformation.setTime(limiter.time());
        requestInformation.setLimitType(limiter.limitType());
    }

    private void buildMethodInfo(RequestInformationBo requestInformation, ProceedingJoinPoint pjp) {
        requestInformation.setAClass(pjp.getTarget().getClass());
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        requestInformation.setMethod(method);
        requestInformation.setArguments(pjp.getArgs());
    }

    /**
     * 从线程上下文中取出请求和响应相关信息
     */
    private void buildRequestInfo(RequestInformationBo requestInformation) throws Exception {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
        requestInformation.setHttpServletRequest(sra.getRequest());
        requestInformation.setHttpServletResponse(sra.getResponse());
        requestInformation.setIp(IPUtil.getIpAddr(requestInformation.getHttpServletRequest()));
    }
}
